在上一章中,我們推導了 RSA 的數學原理與大質數分解的安全性基石。然而,數學上的純粹並不等於工程上的安全。直接使用數學公式 c ≡ m^e mod n 執行的教科書式 RSA (Textbook RSA),在真實工程環境中極度脆弱,易遭受確定性分析、同態攻擊與 Padding Oracle 攻擊。
本章將揭示 RSA 填充機制(Padding)的演進,深入剖析現代非對稱加密的黃金標準——RSA-OAEP (Optimal Asymmetric Encryption Padding),並透過 Java JCA 實作具備抗攻擊能力的生產級程式碼。
在 RSA 加密中,直接對原始位元組進行指數運算是致命的。為了解決安全性漏洞,密碼學界引入了填充(Padding)機制,但填充演算法本身的演進也充滿了血淚史。
為解決 Textbook RSA 的問題,早期推出了 PKCS#1 v1.5 填充格式:在明文前加入固定格式與隨機非零位元組:
EB = 0x00 || 0x02 || Random Padding || 0x00 || Plaintext
然而,1998 年密碼學家 Daniel Bleichenbacher 提出了著名的 Million Message Attack(Padding Oracle 攻擊):
RSA-OAEP (Optimal Asymmetric Encryption Padding, RFC 8017 / PKCS#1 v2.2) 是一種結合了非對稱加工與對稱 Feistel 網路結構的安全填充協議。
+------------------+ +------------------+
| Message (m) | | Optional Label |
+------------------+ +------------------+
| |
v v
[ Padding ] [ Hash (L) ]
| |
+------------+------------+
|
v
+------------------+ +------------------+
| Random Seed | | Data Block (DB)|
+------------------+ +------------------+
| |
| +---------------+ |
+---->| MGF1 (Hash) |----->(+) (XOR)
| +---------------+ |
| v
| MaskedDB
| |
| +---------------+ |
(+)<---| MGF1 (Hash) |<-----+
| +---------------+
v
MaskedSeed
| |
+----------------------------+
|
v
EM = 0x00 || MaskedSeed || MaskedDB ---> 執行 RSA 模次方加密 (c = EMᵉ mod n)
RSA 是一種分組大小受限的演算法,其最大可加密的明文長度 L_max 由以下公式嚴格限制:
L_max = KeyByteLen - 2 * HashByteLen - 2
其中:
| RSA 金鑰長度 (bits) | 模數長度 (Bytes) | OAEP 內部 Hash | Hash 長度 (Bytes) | 最大明文容量L_max |
|---|---|---|---|---|
| 2048 bits | 256 bytes | SHA-256 | 32 bytes | 190 Bytes |
| 3072 bits | 384 bytes | SHA-256 | 32 bytes | 318 Bytes |
| 4096 bits | 512 bytes | SHA-256 | 32 bytes | 446 Bytes |
工程原則:RSA 絕不適合用來直接加密大檔案或長文字。如果資料長度超過 L_max,必須採用 混合加密(Hybrid Encryption) 架構:用 AES-GCM 加密大資料,再用 RSA-OAEP 加密 AES 的對稱金鑰。
在 Java JCA 中,為了確保主雜湊演算法與 MGF1 雜湊演算法皆使用強大的 SHA-256(避免預設退回 SHA-1),必須使用 OAEPParameterSpec 進行精確配置。
import javax.crypto.Cipher;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.MGF1ParameterSpec;
import java.util.Base64;
public class RsaOaepProductionExample {
private static final String RSA_ALGORITHM = "RSA/ECB/OAEPPadding";
private static final int RSA_KEY_SIZE = 3072; // 推薦 3072 bits
public static void main(String[] args) throws Exception {
// 1. 生成 3072-bit RSA KeyPair
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(RSA_KEY_SIZE);
KeyPair keyPair = keyPairGen.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
String originalMessage = "Day 26: Secure RSA-OAEP with SHA-256 and MGF1-SHA256";
System.out.println("原始明文: " + originalMessage);
// 2. RSA-OAEP 加密
byte[] ciphertext = encryptOaep(originalMessage.getBytes(StandardCharsets.UTF_8), publicKey);
System.out.println("加密後密文 (Base64): " + Base64.getEncoder().encodeToString(ciphertext));
// 3. RSA-OAEP 解密
byte[] decryptedBytes = decryptOaep(ciphertext, privateKey);
String decryptedMessage = new String(decryptedBytes, StandardCharsets.UTF_8);
System.out.println("解密成功: " + decryptedMessage);
}
/**
* 生產級 RSA-OAEP 加密 (明確指定 SHA-256 + MGF1-SHA256)
*/
public static byte[] encryptOaep(byte[] plaintext, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
// 顯式配置 OAEP 參數:Primary Hash = SHA-256, MGF1 Hash = SHA-256
OAEPParameterSpec oaepParams = new OAEPParameterSpec(
"SHA-256",
"MGF1",
MGF1ParameterSpec.SHA256,
PSource.PSpecified.DEFAULT
);
cipher.init(Cipher.ENCRYPT_MODE, publicKey, oaepParams);
return cipher.doFinal(plaintext);
}
/**
* 生產級 RSA-OAEP 解密
*/
public static byte[] decryptOaep(byte[] ciphertext, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
// 配置與加密端完全相同的 OAEP 參數
OAEPParameterSpec oaepParams = new OAEPParameterSpec(
"SHA-256",
"MGF1",
MGF1ParameterSpec.SHA256,
PSource.PSpecified.DEFAULT
);
cipher.init(Cipher.DECRYPT_MODE, privateKey, oaepParams);
return cipher.doFinal(ciphertext);
}
}